local service = require "ddcl.service"
local socket = require "ddcl.socket"

local function find_rep(str, rep, max_len)
    local find, epos = string.find(str, rep)
    local del = 0
    local find_str
    if find then
        if find >= max_len then
            return nil, nil
        end
        if find > 1 then
            find_str =  string.sub(str, 1, find-1)
        else
            find_str = ""
        end
        del = epos
    end
    return find_str, del
end

local function read_cache_line(M, rep, max_len, new)
    local cache = M.cache
    local clen = M.clen
    local str, del = nil, 0
    local data = {}
    if not new then
        local fstr, fdel = find_rep(cache[#cache], rep, max_len)
        if fstr then
            str = fstr
            del = clen - string.len(cache[#cache]) + fdel
        end
    else
        local find_len = 0
        for i, v in ipairs(cache) do
            local fstr, fdel = find_rep(v, rep, max_len)
            if fstr then
                str = fstr
                del = del + fdel
                break
            end
            table.insert(data, v)
            del = del + #v
            find_len = find_len + #v
            if find_len >= max_len then
                return nil
            end
        end
    end
    if not str then return nil end
    table.insert(data, str)
    clen = clen - del
    while #cache > 0 do
        local v = cache[1]
        if del >= #v then
            del = del - #v
            table.remove(cache, 1)
        else
            cache[1] = string.sub(v, del + 1)
            del = 0
        end
        if del == 0 then break end
    end
    M.clen = clen
    return table.concat(data, "")
end

local function read_cache(M, len)
    if len == 0 then len = M.clen end
    M.clen = M.clen - len
    local s = {}
    local del = 0
    local cache = M.cache
    for i, v in ipairs(cache) do
        if #v > len then
            table.insert(s, string.sub(v, 1, len))
            v = string.sub(v, len + 1)
            cache[i] = v
            i = i + 1
            len = 0
            break
        else
            table.insert(s, v)
            len = len - #v
            del = del + 1
            if len == 0 then
                i = i + 1
                break
            end
        end
    end

    if del > 0 then
        for i = 1, del do
            table.remove(cache, 1)
        end
    end
    return table.concat(s, "")
end

local function do_read(M, len)
    if M.clen > 0 and M.clen >= len then
        return read_cache(M, len)
    else
        local data = socket.read(M.fd, len - M.clen)
        if not data then
            M.close__()
            return nil
        end
        table.insert(M.cache, data)
        data = table.concat(M.cache, "")
        M.cache = {}
        M.clen = 0
        return data
    end
end

local function do_forward_read(M, len)
    local rds = M.rds
    if #rds > 0 or 
        (len > 0 and len > M.clen) or 
        (len == 0 and M.clen == 0) then
        local rd = {
            len = len,
            co = service.running(),
        }
        table.insert(rds, rd)
        return service.yield()
    else
        return read_cache(M, len)
    end
end

local function do_read_line(M, rep, max_len)
    local rds = M.rds
    local new = true
    local data
    while M.fd do
        if M.clen > 0 then
            data = read_cache_line(M, rep, max_len, new)
            if data then break end
        end
        data = socket.read(M.fd, 0)
        if not data then break end
        table.insert(M.cache, data)
        M.clen = M.clen + #data
        new = false
    end
    if not data then M.close__() end
    return data
end

local function try_read_len(M, rd)
    local data = do_read(M, rd.len)
    if data then
        table.remove(M.rds, 1)
        service.fork(function()
            service.resume(rd.co, data)
        end)
    else
        M.close__()
    end
    return not data
end

local function try_read_line(M, rd)
    local data = read_cache_line(M, rd.rep, rd.max_len, false)
    if data then
        table.remove(M.rds, 1)
        service.fork(function()
            service.resume(rd.co, data)
        end)
        return false
    elseif M.clen >= rd.max_len then
        table.remove(M.rds, 1)
        service.fork(function()
            service.resume(rd.co, nil, "overload max len")
        end)
        return true
    end
    return true
end

local function do_forward_read_line(M, rep, max_len)
    if #M.rds == 0 and M.clen > 0 then
        local data = read_cache_line(M, rep, max_len, true)
        if data then return data end
        if M.clen >= max_len then
            return nil, "overload max len"
        end
    end
    local co = service.running()
    table.insert(M.rds, {
        co = co,
        rep = rep,
        max_len = max_len,
    })
    return service.yield()
end

local function forward_fn(M, data)
    if data then
        table.insert(M.cache, data)
        M.clen = M.clen + #data
        while #M.rds > 0 and M.clen > 0 do
            local rd = M.rds[1]
            if rd.len then
                if try_read_len(M, rd) then
                    break
                end
            elseif rd.rep then
                if try_read_line(M, rd) then
                    break
                end
            end
        end
    else
        M.close__()
    end
end

local MT = {
    __gc = function(self)
        self.close()
    end,
}
MT.__index = MT
return function(fd, opt)
    local M = {
        fd = fd,
        rds = {},
        cache = {},
        clen = 0,
        close_fn = opt.close_fn,
    }
    local forward = opt.forward and true or false
    function M.read_line(rep, max_len)
        rep = rep or "\n"
        max_len = max_len or 204800
        if forward then
            return do_forward_read_line(M, rep, max_len)
        else
            return do_read_line(M, rep, max_len)
        end
    end

    function M.read(len)
        len = len or 0
        if forward then
            return do_forward_read(M, len)
        else
            local data = do_read(M, len)
            return data
        end
    end

    function M.close()
        M.close__()
    end

    function M.close__()
        if not M.fd then
            return
        end
        pcall(socket.close, M.fd)
        M.fd = nil
        for i, v in ipairs(M.rds) do
            local co = v.co
            service.fork(function()
                service.resume(co, nil, "fd closed")
            end)
        end
        M.rds = {}
        if M.close_fn then
            M.close_fn(M)
        end
    end

    function M.send(data)
        if M.fd then
            if not pcall(socket.send, M.fd, data) then
                M.close__()
            end
        end
    end

    function M.sendraw(p, sz)
        if M.fd then
            if not pcall(socket.sendraw, M.fd, p, sz) then
                M.close__()
            end
        end
    end

    if forward then
        socket.forward(fd, function(msg, data)
            forward_fn(M, data)
        end)
    end
    return setmetatable(M, MT)
end
